// --- Copyright (c) 2006-2008 Stefan Kyntchev ---
// This software is written and copyrighted by Stefan Kyntchev 
// and BeyondPod Team members. All rights are reserved.
// Author contact: support@beyondpod.mobi
// ------------------------------------------------------------------------
// This file is part of BeyondPod RSS Feed Reader and Podcast manager
// (www.codeplex.com/BeyondPod) 
// BeyondPod is free software: you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published by 
// the Free Software Foundation, either version 3 of the License, or 
// (at your option) any later version. 
//  
// BeyondPod is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
// GNU General Public License for more details. 
//  
// You should have received a copy of the GNU General Public License
// along with BeyondPod. If not, see <http://www.gnu.org/licenses/>
// ------------------------------------------------------------------------
// COPYRIGHT NOTICE: This copyright notice may NOT be removed, obscured or modified 
// without written consent from the author.
// --- End copyright notice --- 


using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using svs.Mobile.BeyondMedia.FeedCore;
using svs.Mobile.BeyondMedia.FeedCore.RSS.Entities;
using svs.Mobile.BeyondMedia.FeedCore.RSS.Parsers;

namespace svs.Mobile.BeyondMedia.FeedCore.RSS
{
	public static class RssFeedCache
	{
		#region Constructors

		static RssFeedCache()
		{
			if (!Directory.Exists(Configuration.RssCachePath))
				Directory.CreateDirectory(Configuration.RssCachePath);
		}

		#endregion

		#region Public Methods

		public static long GetCachedFileSize(Uri url)
		{
			FileInfo file = new FileInfo(GetFeedDownloadPath(url));

			//If the old feed exists, check if a new version is available
			if (file.Exists)
			{
				return file.Length;
			}

			return -1;
		}


		public static DateTime? GetCachedFileDate(Uri url)
		{
			FileInfo file = new FileInfo(GetFeedDownloadPath(url));

			//If the old feed exists, check if a new version is available
			if (file.Exists)
			{
				return file.LastWriteTime;
			}

			return null;
		}

		public static string GetFeedDownloadPath(Uri url)
		{
			if (url == null)
				throw new ArgumentNullException("GetFeedDownloadPath - called with Invalid Url");

			return Path.Combine(Configuration.RssCachePath,
			                    string.Format(CultureInfo.InvariantCulture,
			                    	"{0}_{1:X}.xml",
			                    	url.Host.Replace('.', '_'),
			                    	url.GetHashCode()));
				
		}

		public static string GetFeedTempPath(Uri url)
		{
			return GetFeedDownloadPath(url) + ".temp";
		}

		public delegate bool LoadFilterDelegate(RssFeed feed);

		/// <summary>
		/// Loads the entire feed with all the items
		/// </summary>
		/// <param name="feed"></param>
		/// <returns></returns>
		public static RssFeed LoadFromCache(Feed feed)
		{
			return LoadFromCacheInternal(feed, null, RssFeedParser.FeedParseLevel.Complete);
		}

		/// <summary>
		/// Loads only the Channel element and the first item
		/// </summary>
		/// <param name="feed"></param>
		/// <returns></returns>
		public static RssFeed PeekFromCache(Feed feed, RssFeedParser.FeedParseLevel parseLevel)
		{
			return LoadFromCacheInternal(feed, null,parseLevel);
		}

		public static void DeletePrimaryFeedCacheFor(Feed feed)
		{
			if (!feed.HasUri)
				return;

			string file = GetFeedDownloadPath(feed.FeedUri);

			DeleteFile(file);
		}

		public static void DeleteTempFeedCacheFor(Feed feed)
		{
			if (!feed.HasUri)
				return;

			string tempFile = GetFeedTempPath(feed.FeedUri);

			DeleteFile(tempFile);
		}

		/// <summary>
		/// Deletes the cached version for a feed
		/// </summary>
		/// <param name="feed"></param>
		public static void DeleteFeedCacheFor(Feed feed)
		{
			DeletePrimaryFeedCacheFor(feed);
			DeleteTempFeedCacheFor(feed);
		}

		/// <summary>
		/// Deletes teh cached version of all files
		/// </summary>
		/// <returns></returns>
		public static int DeleteCacheForAllFeeds()
		{
			string[] files = Directory.GetFiles(Configuration.RssCachePath);

			int failedCount = 0;
			foreach (string file in files)
			{
				try
				{
					File.Delete(file);
				}
				catch
				{
					failedCount++;
				}
			}

			return failedCount;
		}

		public static void CopyTempToPrimaryForFeed(Feed feed)
		{
			string temp = GetFeedTempPath(feed.FeedUri);
			string file = GetFeedDownloadPath(feed.FeedUri);

			try
			{
				if(File.Exists(file))
					File.Delete(file);
			}
			catch (Exception e)
			{
				CoreHelper.WriteLogEntry(string.Format("Unable to delete {0}! Reason: {1}", file, e.Message));
			}

			try
			{
				File.Move(temp, file);
			}
			catch (Exception e)
			{
				CoreHelper.WriteLogEntry(string.Format("Unable to move {0} to {1}! Reason: {2}", temp, file, e.Message));
			}
		}

		public static void TouchFeedCacheFor(Feed feed)
		{
			
				if (!feed.HasUri)
					return;

				string file = GetFeedDownloadPath(feed.FeedUri);
				CoreHelper.TouchFile(file);
		}

		public static void SyncFeedReadStatus(Feed feed)
		{
			if(feed == null || !feed.HasUrl)
				return;

			var rssFeed = PeekFromCache(feed, RssFeedParser.FeedParseLevel.ItemEssentials);

			if(rssFeed != null)
				rssFeed.SyncOwnerFeedReadStatus();

		}

		public static void SyncFeedReadStatusForTrack(Track track)
		{
			if (track == null || !track.ParentFeed.HasUrl)
				return;

			var rssFeed = PeekFromCache(track.ParentFeed, RssFeedParser.FeedParseLevel.ItemAndEnclosureEssentials);

			if (rssFeed != null)
			{
				RssEnclosure encl = rssFeed.GetEnclosureByLocalFileName(track.TrackPath);
				if (encl != null)
				{
					encl.Parent.Read = track.IsPlayed;
					rssFeed.SyncOwnerFeedReadStatus();
				}
			}
		}

		#endregion

		#region Private Methods

		internal static XmlReader CreateReader(string file)
		{
			XmlReader reader = null;
			try
			{
				reader = CreateReaderInternal(file);
				do
				{
					reader.Read(); //try to read something
				} while (reader.ReadState == ReadState.Interactive && reader.NodeType != XmlNodeType.Element);
			}
			catch (Exception)
			{
				if (reader != null)
					reader.Close();

				PatchUnsuportedXML(file);
				reader = CreateReaderInternal(file);
			}

			return reader;
		}

		internal static XmlReader CreateReaderInternal(string file)
		{
			XmlReaderSettings settings = new XmlReaderSettings();
			settings.XmlResolver = null;
			settings.IgnoreProcessingInstructions = true;
			settings.IgnoreComments = true;
			settings.ConformanceLevel = ConformanceLevel.Auto;
			settings.IgnoreWhitespace = true;
			settings.ValidationType = ValidationType.None;

			return XmlReader.Create(file, settings);
		}

		private static void PatchUnsuportedXML(string file)
		{
			//Try to patch the iso-8859 encoding with ascii;
			using (FileStream sFile = new FileStream(file, FileMode.Open, FileAccess.ReadWrite))
			{
				byte[] buffer = new byte[2048];

				//Read the first 2k from the file
				int buffRead = sFile.Read(buffer, 0, buffer.Length);
				string str = Encoding.ASCII.GetString(buffer, 0, buffer.Length);

				//Patch Doctype
				Regex regexDocType = new Regex(
					@"(?<Doc><!DOCTYPE[^\]]*\]>)|(?<Doc><!DOCTYPE[^>]*>)",
					RegexOptions.IgnoreCase
					| RegexOptions.Multiline
					| RegexOptions.IgnorePatternWhitespace
					| RegexOptions.Compiled
					);

				Match mDocType = regexDocType.Match(str);

				if (mDocType.Groups["Doc"].Success)
				{
					string doc = mDocType.Groups["Doc"].Value;
					string whitespace = "".PadRight(doc.Length);

					//Delete the !DOCTYPE
					sFile.Seek(mDocType.Groups["Doc"].Captures[0].Index, SeekOrigin.Begin);
					sFile.Write(Encoding.ASCII.GetBytes(whitespace), 0, whitespace.Length);

					//Re read the buffer
					sFile.Seek(0, SeekOrigin.Begin);
					buffRead = sFile.Read(buffer, 0, buffer.Length);
					str = Encoding.ASCII.GetString(buffer, 0, buffer.Length);
				}

				//Patch iso-8859-1 encoding
				Regex regex = new Regex(
					@"\<\?xml.*encoding\s*=\s*(?<Enc>['""]iso-8859-1['""]).*\?\>",
					RegexOptions.IgnoreCase
					| RegexOptions.Multiline
					| RegexOptions.IgnorePatternWhitespace
					| RegexOptions.Compiled
					);

				Match m = regex.Match(str);

				if (m.Groups["Enc"].Success)
				{
					string iso = m.Groups["Enc"].Value;
					string win1252 = "'Windows-1252'";
					string tempFile = file + ".tmp";

					try
					{
						using (FileStream converted = new FileStream(tempFile, FileMode.Create, FileAccess.Write))
						{
							converted.Write(buffer, 0, m.Groups["Enc"].Captures[0].Index);
							converted.Write(Encoding.ASCII.GetBytes(win1252), 0, win1252.Length);

							int pastEnc = m.Groups["Enc"].Captures[0].Index + iso.Length;
							converted.Write(buffer, pastEnc, buffRead - pastEnc);

							do
							{
								buffRead = sFile.Read(buffer, 0, buffer.Length);
								converted.Write(buffer, 0, buffRead);
							} while (buffRead > 0);

							sFile.Close();
							converted.Close();
							File.Delete(file);
							File.Move(tempFile, file);
						}
					}
					catch (Exception ex)
					{
						CoreHelper.WriteLogEntry("Unable to patch file with 'iso-8859-1' encoding. Reason: " + ex.Message);
					}
				}

				sFile.Close();
			}
		}

		private static void DeleteFile(string file)
		{
			if (File.Exists(file))
			{
				try
				{
					File.Delete(file);
				}
				catch (Exception ex)
				{
					CoreHelper.WriteLogEntry(
						string.Format("Unable to delete {0}! Reason: {1}", file, ex.Message));
				}
			}
		}

		private static RssFeed LoadFromCacheInternal(Feed feed, LoadFilterDelegate shouldStop, RssFeedParser.FeedParseLevel parseLevel )
		{
			CoreHelper.WriteTraceEntry("Loading Feed XML...");

			RssFeed retVal = null;

			if (!feed.HasUri)
				throw new ApplicationException("The URL of the feed was not provided or is invalid!");

			string file = GetFeedDownloadPath(feed.FeedUri);

			lock (file)
			{
				if (File.Exists(file))
				{
					XmlReader tr = null;
					try
					{
						tr = CreateReader(file);

						RssFeedParser pb = new RssFeedParser(feed.FeedPath, feed.ForceUniqueTrackNames, feed.ID, shouldStop, parseLevel);

						do
						{
							//Handle the XML
							pb.Handle(tr);

						} while (tr.ReadState != ReadState.Closed && tr.Read());

						retVal = pb.Feed;
						retVal.LastRetrievalDate = File.GetLastWriteTime(file).ToUniversalTime();
					}
					catch (CoreHelper.EncounteredOpmlFeedException)
					{
						throw;
					}
					catch (Exception ex)
					{
						CoreHelper.WriteLogEntry(string.Format("Unable to read the cache feed data for feed {0}! Error: {1}", feed.FeedUrl, ex));
						throw new ApplicationException(
							"This feed does not point to a valid RSS feed!\nTry updating or check the URL in the feed properties!");
					}
					finally
					{
						if (tr != null)
							tr.Close();

						CoreHelper.WriteTraceEntry("DONE Loading Feed XML!");
					}
				}
			}

			if(feed.ForceFeedItemSort && retVal != null)
				retVal.Items.Sort(CompareFeedItemsByDate);

			return retVal;
		}


		private static int CompareFeedItemsByDate(RssFeedItem lhs, RssFeedItem rhs)
		{
			if (lhs == null && rhs == null)
				return 0;

			if (rhs == null)
				return -1;

			if (lhs == null)
				return 1;

			if (!lhs.PubDate.HasValue && !rhs.PubDate.HasValue)
				return 0;

			if (!rhs.PubDate.HasValue)
				return -1;

			if (!lhs.PubDate.HasValue)
				return 1;

			return rhs.PubDate.Value.CompareTo(lhs.PubDate.Value);
		}


		/// <summary>
		/// Stops reading at the first item
		/// </summary>
		/// <param name="feed"></param>
		/// <returns></returns>
		private static bool ChannelOnlyFilter(RssFeed feed)
		{
			return feed.Items.Count == 0;
		}

		#endregion
	}
}